package vip.liux.backend.application.serviceImpl.pay;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import vip.liux.application.dots.ListResultDto;
import vip.liux.application.dots.PagedResultDto;
import vip.liux.backend.application.service.pay.*;
import vip.liux.backend.application.service.pay.dto.app.PayAppDto;
import vip.liux.backend.application.service.pay.dto.channel.PayChannelDto;
import vip.liux.backend.application.service.pay.dto.order.PayOrderDto;
import vip.liux.backend.application.service.pay.dto.refund.PayRefundCreateDto;
import vip.liux.backend.application.service.pay.dto.refund.PayRefundDetailsDto;
import vip.liux.backend.application.service.pay.dto.refund.PayRefundPageItemDto;
import vip.liux.backend.application.service.pay.dto.refund.PayRefundQueryDto;
import vip.liux.backend.infrastructure.code.BizCodeGenerator;
import vip.liux.contracts.infrastructure.pay.PayClient;
import vip.liux.contracts.infrastructure.pay.dto.refund.ClientPayRefundDto;
import vip.liux.contracts.infrastructure.pay.dto.refund.PayRefundUnifiedDto;
import vip.liux.contracts.models.pay.PayRefund;
import vip.liux.contracts.repositories.pay.PayRefundRepository;
import vip.liux.contracts.shared.enums.pay.PayNotifyType;
import vip.liux.contracts.shared.enums.pay.PayOrderStatus;
import vip.liux.contracts.shared.enums.pay.PayRefundStatus;

import java.util.List;
import java.util.Optional;

import static vip.liux.application.PageKit.toPage;
import static vip.liux.application.PageKit.toPaged;
import static vip.liux.backend.infrastructure.utils.ListJoinKit.backFill;
import static vip.liux.contracts.shared.enums.ErrorCode.*;
import static vip.liux.contracts.shared.util.ServiceExceptionUtil.exception;
import static vip.liux.core.utils.JsonUtils.toJson;

/**
 * 退款订单 Service 实现类
 *
 * @author jason
 */
@Service
@Slf4j
@Validated
public class PayRefundServiceImpl implements PayRefundService, ApplicationContextAware {


    @Resource
    private PayRefundRepository refundRepo;
    @Resource
    private BizCodeGenerator codeGenerator;
    @Resource
    private PayOrderService orderService;
    @Resource
    private PayAppService appService;
    @Resource
    private PayChannelService channelService;
    @Resource
    private PayNotifyService notifyService;

    @Override
    public PayRefundDetailsDto getRefund(Long id) {
        PayRefundDetailsDto refund = AutoMapperProfile.INSTANCE.convert(refundRepo.findById(id).orElseThrow(() -> exception(REFUND_NOT_FOUND)));
        Optional.ofNullable(refund)
                .ifPresent(o -> Optional.ofNullable(
                        appService.getApp(o.getAppId())).ifPresent(p -> o.setAppName(p.getName())
                ));
        return refund;
    }

    @Override
    public PayRefundDetailsDto getRefundByRefundNo(String refundNo) {
        PayRefundDetailsDto refund = AutoMapperProfile.INSTANCE.convert(refundRepo.findByRefundNo(refundNo).orElseThrow(() -> exception(REFUND_NOT_FOUND)));
        Optional.ofNullable(refund)
                .ifPresent(o -> Optional.ofNullable(
                        appService.getApp(o.getAppId())).ifPresent(p -> o.setAppName(p.getName())
                ));
        return refund;
    }

    @Override
    public Long getRefundCountByAppId(Long appId) {
        return refundRepo.countByAppId(appId);
    }

    @Override
    public PagedResultDto<PayRefundPageItemDto> getRefundPage(PayRefundQueryDto input) {
        PagedResultDto<PayRefundPageItemDto> pagedResult = toPaged(refundRepo.findPage(
                input.getAppId(),
                input.getChannelCode(),
                input.getMerchantOrderId(),
                input.getMerchantRefundId(),
                input.getChannelOrderNo(),
                input.getChannelRefundNo(),
                input.getStatus(),
                input.getStart(),
                input.getEnd(),
                toPage(input)
        ).map(AutoMapperProfile.INSTANCE::convertPage));
        if (pagedResult.getTotalCount() > 0) {
            // 补充应用信息
            ListResultDto<PayAppDto> appList = appService.getAppList(pagedResult.getItems().stream().map(PayRefundPageItemDto::getAppId).toList());

            backFill(
                    pagedResult.getItems(),
                    appList.getItems(),
                    PayRefundPageItemDto::getAppId,
                    PayAppDto::getId,
                    (order, app) -> order.setAppName(app.getName())
            );
        }
        return pagedResult;
    }

    @Override
    public ListResultDto<PayRefundPageItemDto> getRefundList(PayRefundQueryDto input) {
        return new ListResultDto<>(refundRepo.findPage(
                input.getAppId(),
                input.getChannelCode(),
                input.getMerchantOrderId(),
                input.getMerchantRefundId(),
                input.getChannelOrderNo(),
                input.getChannelRefundNo(),
                input.getStatus(),
                input.getStart(),
                input.getEnd(),
                Pageable.unpaged()
        ).map(AutoMapperProfile.INSTANCE::convertPage).getContent());
    }
    private ApplicationContext applicationContext;

    @Override
    public String createRefund(PayRefundCreateDto input) {
        // 1.1 校验 App
        PayAppDto app = appService.validPayApp(input.getAppKey());
        // 1.2 校验支付订单
        PayOrderDto order = validatePayOrderCanRefund(input, app.getId());
        // 1.3 校验支付渠道是否有效
        PayChannelDto channel = channelService.validPayChannel(order.getChannelId());
        PayClient<?> client = channelService.getPayClient(channel.getId());
        if (client == null) {
            log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
            throw exception(CHANNEL_NOT_FOUND);
        }
        // 1.4 校验退款订单是否已经存在
        PayRefund refund = refundRepo.findByAppIdAndMerchantRefundId(app.getId(), input.getMerchantRefundId());
        if (refund != null) {
            throw exception(REFUND_EXISTS);
        }

        // 2.1 插入退款单
        String refundNo = codeGenerator.generateRefundNo();

        refund = new PayRefund()
                .setMerchantRefundId(input.getMerchantRefundId())
                .setMerchantOrderId(input.getMerchantOrderId())
                .setReason(input.getReason())
                .setUserIp(input.getUserIp())
                .setRefundNo(refundNo)
                .setAppId(app.getId())
                .setOrderNo(order.getOrderNo())
                .setChannelId(order.getChannelId())
                .setChannelCode(order.getChannelCode())
                // 商户相关的字段
                .setNotifyUrl(app.getRefundNotifyUrl())
                // 渠道相关字段
                .setChannelOrderNo(order.getChannelOrderNo())
                // 退款相关字段
                .setStatus(PayRefundStatus.WAITING)
                .setPayPrice(order.getPrice())
                .setRefundPrice(input.getPrice());
        refundRepo.save(refund);
        try {
            // 2.2 向渠道发起退款申请
            PayRefundUnifiedDto unifiedDto = new PayRefundUnifiedDto()
                    .setPayPrice(order.getPrice())
                    .setRefundPrice(input.getPrice())
                    .setOutTradeNo(order.getOrderNo())
                    .setOutRefundNo(refund.getRefundNo())
                    .setNotifyUrl(genChannelRefundNotifyUrl(channel))
                    .setReason(input.getReason());
            ClientPayRefundDto refundRespDTO = client.unifiedRefund(unifiedDto);
            // 2.3 处理退款返回
            getSelf().notifyRefund(channel, refundRespDTO);
        } catch (Throwable e) {
            // 注意：这里仅打印异常，不进行抛出。
            // 原因是：虽然调用支付渠道进行退款发生异常（网络请求超时），实际退款成功。这个结果，后续通过退款回调、或者退款轮询补偿可以拿到。
            // 最终，在异常的情况下，支付中心会异步回调业务的退款回调接口，提供退款结果
            log.error("[createPayRefund][退款 id({}) requestDTO({}) 发生异常]",
                    refund.getId(), input, e);
        }

        // 返回退款编号
        return refund.getRefundNo();
    }

    /**
     * 校验支付订单是否可以退款
     *
     * @param reqDTO 退款申请信息
     * @return 支付订单
     */
    private PayOrderDto validatePayOrderCanRefund(PayRefundCreateDto reqDTO, Long appId) {
        PayOrderDto order = orderService.getOrder(appId, reqDTO.getMerchantOrderId());
        if (order == null) {
            throw exception(PAY_ORDER_NOT_FOUND);
        }
        // 校验状态，必须是已支付、或者已退款
        if (!PayOrderStatus.isSuccessOrRefund(order.getStatus())) {
            throw exception(PAY_ORDER_REFUND_FAIL_STATUS_ERROR);
        }

        // 校验金额，退款金额不能大于原定的金额
        if (reqDTO.getPrice().add(order.getRefundPrice()).compareTo(order.getPrice()) > 0) {
            throw exception(REFUND_PRICE_EXCEED);
        }
        // 是否有退款中的订单
        if (refundRepo.countByAppIdAndOrderNoAndStatus(appId, order.getOrderNo(), PayRefundStatus.WAITING) > 0) {
            throw exception(REFUND_HAS_REFUNDING);
        }
        return order;
    }

    /**
     * 根据支付渠道的编码，生成支付渠道的回调地址
     *
     * @param channel 支付渠道
     * @return 支付渠道的回调地址  配置地址 + "/" + channel id
     */
    private String genChannelRefundNotifyUrl(PayChannelDto channel) {
        return "https://wechat.liux.vip" + "/" + channel.getId();
    }

    @Override
    public void notifyRefund(Long channelId, ClientPayRefundDto notify) {
        // 校验支付渠道是否有效
        PayChannelDto channel = channelService.validPayChannel(channelId);
        // 更新退款订单
        getSelf().notifyRefund(channel, notify);
    }

    /**
     * 通知并更新订单的退款结果
     *
     * @param channel 支付渠道
     * @param notify  通知
     */
    // 注意，如果是方法内调用该方法，需要通过 getSelf().notifyRefund(channel, notify) 调用，否则事务不生效
    @Transactional(rollbackFor = Exception.class)
    public void notifyRefund(PayChannelDto channel, ClientPayRefundDto notify) {
        // 情况一：退款成功
        if (PayRefundStatus.isSuccess(notify.getStatus())) {
            notifyRefundSuccess(channel, notify);
            return;
        }
        // 情况二：退款失败
        if (PayRefundStatus.isFailure(notify.getStatus())) {
            notifyRefundFailure(channel, notify);
        }
    }

    private void notifyRefundSuccess(PayChannelDto channel, ClientPayRefundDto notify) {
        // 1.1 查询 PayRefundDO
        PayRefund refund = refundRepo.findByAppIdAndRefundNo(
                channel.getAppId(), notify.getOutRefundNo()).orElseThrow(() -> exception(REFUND_NOT_FOUND));

        if (PayRefundStatus.isSuccess(refund.getStatus())) { // 如果已经是成功，直接返回，不用重复更新
            log.info("[notifyRefundSuccess][退款订单({}) 已经是退款成功，无需更新]", refund.getId());
            return;
        }
        if (!PayRefundStatus.WAITING.equals(refund.getStatus())) {
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        // 1.2 更新 PayRefundDO
        int updateCounts = refundRepo.updateRefundSuccess(
                refund.getId(),
                refund.getStatus(),
                notify.getSuccessTime(),
                notify.getChannelRefundNo(),
                PayRefundStatus.SUCCESS,
                toJson(notify)
        );
        if (updateCounts == 0) { // 校验状态，必须是等待状态
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        log.info("[notifyRefundSuccess][退款订单({}) 更新为退款成功]", refund.getId());

        // 2. 更新订单
        orderService.updateOrderRefundPrice(refund.getOrderNo(), refund.getRefundPrice());

        // 3. 插入退款通知记录
        notifyService.createPayNotifyTask(PayNotifyType.REFUND, refund.getRefundNo());
    }

    private void notifyRefundFailure(PayChannelDto channel, ClientPayRefundDto notify) {
        // 1.1 查询 PayRefundDO
        PayRefund refund = refundRepo.findByAppIdAndRefundNo(
                channel.getAppId(), notify.getOutRefundNo()).orElseThrow(() -> exception(REFUND_NOT_FOUND));

        if (PayRefundStatus.isFailure(refund.getStatus())) { // 如果已经是成功，直接返回，不用重复更新
            log.info("[notifyRefundSuccess][退款订单({}) 已经是退款关闭，无需更新]", refund.getId());
            return;
        }
        if (!PayRefundStatus.WAITING.equals(refund.getStatus())) {
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        // 1.2 更新 PayRefund
        int updateCounts = refundRepo.updateRefundFailure(
                refund.getId(),
                refund.getStatus(),
                notify.getChannelRefundNo(),
                PayRefundStatus.FAILURE,
                toJson(notify),
                notify.getChannelErrorCode(),
                notify.getChannelErrorMsg()
        );
        if (updateCounts == 0) { // 校验状态，必须是等待状态
            throw exception(REFUND_STATUS_IS_NOT_WAITING);
        }
        log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId());

        // 2. 插入退款通知记录
        notifyService.createPayNotifyTask(PayNotifyType.REFUND,
                refund.getRefundNo());
    }

    @Override
    public int syncRefund() {
        // 1. 查询指定创建时间内的待退款订单
        List<PayRefund> refunds = refundRepo.findByStatus(PayRefundStatus.WAITING);
        if (CollectionUtils.isEmpty(refunds)) {
            return 0;
        }
        // 2. 遍历执行
        int count = 0;
        for (PayRefund refund : refunds) {
            count += syncRefund(refund) ? 1 : 0;
        }
        return count;
    }

    /**
     * 同步单个退款订单
     *
     * @param refund 退款订单
     * @return 是否同步到
     */
    private boolean syncRefund(PayRefund refund) {
        try {
            // 1.1 查询退款订单信息
            PayClient<?> payClient = channelService.getPayClient(refund.getChannelId());
            if (payClient == null) {
                log.error("[syncRefund][渠道编号({}) 找不到对应的支付客户端]", refund.getChannelId());
                return false;
            }
            ClientPayRefundDto refundDto = payClient.getRefund(refund.getOrderNo(), refund.getRefundNo());
            // 1.2 回调退款结果
            notifyRefund(refund.getChannelId(), refundDto);

            // 2. 如果同步到，则返回 true
            return PayRefundStatus.isSuccess(refundDto.getStatus())
                    || PayRefundStatus.isFailure(refundDto.getStatus());
        } catch (Throwable e) {
            log.error("[syncRefund][refund({}) 同步退款状态异常]", refund.getId(), e);
            return false;
        }
    }

    /**
     * 获得自身的代理对象，解决 AOP 生效问题
     *
     * @return 自己
     */
    private PayRefundServiceImpl getSelf() {
        return applicationContext.getBean(getClass());
    }

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
